// 
// SoapTrace.Net - A soap message monitoring utility. 
// 
// Copyright (C) 2004-2009 Matthew Ward
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// Matthew Ward (mrward@users.sourceforge.net)
// 

using SoapTrace.Core.Diagnostics;
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace SoapTrace.Core.Communication
{
	/// <summary>
	/// A Tcp proxy server.
	/// </summary>
	public class ProxyServer : IDisposable
	{
		/// <summary>
		/// Disposed flag.
		/// </summary>
		private bool m_Disposed;
		
		/// <summary>
		/// The proxy server's listening socket.
		/// </summary>
		private Socket m_ListeningSocket;
		
		/// <summary>
		/// The maximum length of the pending connection's queue.
		/// </summary>
		private int m_Backlog = 1;
		
		/// <summary>
		/// The IP address of the proxy server.
		/// </summary>
		private string m_IPAddress = "localhost";
		
		/// <summary>
		/// The main processing thread.
		/// </summary>
		private Thread m_Thread;
		
		/// <summary>
		/// Flag indicating whether the main processing thread should
		/// run.
		/// </summary>
		private bool m_Running;
		
		/// <summary>
		/// Socket processing timeout.
		/// </summary>
		private int m_Timeout = 10;
		
		/// <summary>
		/// The remote server's IP address.
		/// </summary>
		private string m_RemoteServerIPAddress = String.Empty;
		
		/// <summary>
		/// The remote server's port.
		/// </summary>
		private int m_RemoteServerPort;
		
		/// <summary>
		/// The client server proxy channels.
		/// </summary>
		private ProxyChannelCollection m_Channels = new ProxyChannelCollection( );
		
		/// <summary>
		/// Creates a new instance of this class.
		/// </summary>
		public ProxyServer( )
		{
		}
		
		/// <summary>
		/// Starts the proxy server.
		/// </summary>
		/// <param name="port">The port the proxy server will listen on.</param>
		/// <param name="remoteServerIPAddress">The remote server the proxy
		/// will forward connections and client messages to.</param>
		/// <param name="remoteServerPort">The remote server's port.</param>
		public void Start( int port,
		                   string remoteServerIPAddress,
		                   int remoteServerPort )
		{			
			// Create proxy server listening socket.
			CreateListeningSocket( port );
			
			// Save remote server information.
			m_RemoteServerIPAddress = remoteServerIPAddress;
			m_RemoteServerPort = remoteServerPort;
			
			// Start processing thread.
			StartProcessingThread( );
		}
		
		/// <summary>
		/// Stops the proxy server.
		/// </summary>
		public void Stop( )
		{			
			try
			{
				// Stop processing thread.
				StopProcessingThread( );
				
				CloseListeningSocket( );
			}
			catch( SocketException Ex )
			{
				Logger.LogError( String.Concat( "ProxyServer::Stop ",
				                                Ex.ToString( ) ) );
			}
		}
			
		/// <summary>
		/// Disposes this class.
		/// </summary>
		public void Dispose( )
		{
			Dispose( true );

			GC.SuppressFinalize( this );
		}	
		
		/// <summary>
		/// Creates a <see cref="TcpSocketConnection"/> object for a newly accepted
		/// socket.
		/// </summary>
		/// <param name="socket">The accepted socket.</param>
		/// <returns>A new <see cref="TcpSocketConnection"/>.</returns>
		/// <remarks>Derived classes can override this to 
		/// create their own tcp connection classes so they can do
		/// message specific processing.  The default implementation returns
		/// a new <see cref="TcpSocketConnection"/> object.</remarks>
		protected virtual TcpSocketConnection CreateClientConnection( Socket socket )
		{
			return new TcpSocketConnection( socket );
		}

		/// <summary>
		/// Creates a <see cref="TcpSocketConnection"/> object to be used
		/// as the connection to the server.
		/// </summary>
		/// <param name="address">The server's IP address.</param>
		/// <param name="port">The server's port.</param>
		/// <returns>A new <see cref="TcpSocketConnection"/>.</returns>
		/// <remarks>Derived classes can override this to 
		/// create their own tcp connection classes so they can do
		/// message specific processing.  The default implementation returns
		/// a new <see cref="TcpSocketConnection"/> object.</remarks>
		protected virtual TcpSocketConnection CreateServerConnection( string address, int port )
		{
			return new TcpSocketConnection( address, port );
		}
		
		/// <summary>
		/// Creates a listening socket at the specified port.
		/// </summary>
		/// <param name="port">The port to listen on.</param>
		private void CreateListeningSocket( int port )
		{
			m_ListeningSocket = new Socket( AddressFamily.InterNetwork,
			                                SocketType.Stream,
			                                ProtocolType.IP );
			
			IPHostEntry HostEntry = Dns.GetHostEntry( m_IPAddress );
			IPAddress IPAddress = TcpSocket.GetIPAddress( HostEntry );
			IPEndPoint EndPoint = new IPEndPoint( IPAddress, port );
			
			m_ListeningSocket.Bind( EndPoint );
			m_ListeningSocket.Listen( m_Backlog );
			
		}
		
		/// <summary>
		/// Starts the main socket processing thread.
		/// </summary>
		private void StartProcessingThread( )
		{
			m_Thread = new Thread( new ThreadStart( ProcessSockets ) );
			m_Running = true;
			m_Thread.Start( );
		}
		
		/// <summary>
		/// Stops the socket processing thread.
		/// </summary>
		private void StopProcessingThread( )
		{
			if( m_Thread != null )
			{
				m_Running = false;
				m_Thread.Join( );
				m_Thread = null;
			}
		}
		
		/// <summary>
		/// Processes the connected client sockets and the listening
		/// socket.
		/// </summary>
		/// <remarks>This handles reading client requests, forwarding
		/// the request to the server, sending server responses to the client
		/// and accepting new client connections.</remarks>
		private void ProcessSockets( )
		{
			while( m_Running )
			{
				try
				{
					if( IsConnectionPending( ) )
					{
						AcceptConnection( );
					}

					if( ClientsConnected( ) )
					{
						ProcessAllChannels( );
					}

					// Make sure this thread does not hog the
					// CPU.

					Thread.Sleep( m_Timeout );
				}
				catch( SocketException Ex )
				{
					Logger.LogError( String.Concat( "ProxyServer::ProcessSockets ",
					                               Ex.ToString( ) ) );
				}
			}
		}
						
		/// <summary>
		/// Accepts a client connection to the proxy server.
		/// </summary>
		private void AcceptConnection( )
		{
			Logger.LogDebug( "ProxyServer::AcceptConnection in" );
			
			TcpSocketConnection ClientConnection = null;
			ProxyChannel Channel = null;
 
			try
			{
				// Accept new client.
				
				ClientConnection = AcceptTcpClientConnection( );
				
				// Create connection to remote server.
				TcpSocketConnection ServerConnection = 
					CreateServerConnection( m_RemoteServerIPAddress,
					                       m_RemoteServerPort );
				
				// Create a proxy server channel.
				Channel = new ProxyChannel( ClientConnection, ServerConnection );
				
				m_Channels.Add( Channel );
				
				Logger.LogDebug( "ProxyServer::AcceptConnection out" );
			}
			catch( SocketException Ex )
			{
				Logger.LogError( String.Concat( "ProxyServer::AcceptConnection ",
				                               Ex.ToString( ) ) );
				
				// Clean up here.

				if( ClientConnection != null )
				{
					ClientConnection.Close( );
				}
			}
		}

		/// <summary>
		/// Gets whether there are any clients waiting for a connection
		/// request to be accepted.
		/// </summary>
		/// <returns>A flag indicating if there are one or more client
		/// connections to be accepted.</returns>
		private bool IsConnectionPending( )
		{
			return m_ListeningSocket.Poll( 0, SelectMode.SelectRead );
		}

		/// <summary>
		/// Gets whether there are clients connected to this server.
		/// </summary>
		/// <returns>A flag indicating whether there are clients
		/// connected to this server.</returns>
		private bool ClientsConnected( )
		{
			bool ClientsConnected = false;

			if( m_Channels.Count > 0 )
			{
				ClientsConnected = true;
			}

			return ClientsConnected;
		}

		/// <summary>
		/// Processes open client-server channels.  
		/// </summary>
		/// <remarks>Handles readig client requests, forwarding requests to the
		/// server, reading server responses and forwarding server responses
		/// to the client.</remarks>
		private void ProcessAllChannels( )
		{
			// Loop around the channels in reverse order so
			// we can remove a channel whilst looping.

			for( int CurrentChannelIndex = m_Channels.Count - 1; 
				CurrentChannelIndex >= 0;
				--CurrentChannelIndex )
			{
				ProcessChannel( m_Channels[ CurrentChannelIndex ] );
			}
		}

		/// <summary>
		/// Processes an individual proxy channel.
		/// </summary>
		/// <param name="channel">The channel to process.</param>
		private void ProcessChannel( ProxyChannel channel )
		{
			try
			{
				channel.Process( );

				if( channel.IsOpen == false )
				{
					CloseChannel( channel );
				}
			}
			catch( SocketException Ex )
			{
				Logger.LogError( String.Concat( "ProxyServer::ProcessChannel ",
				                               Ex.ToString( ) ) );

				CloseChannel( channel );
			}
		}

		/// <summary>
		/// Closes the channel and removes it from the collection of
		/// connected channels.
		/// </summary>
		/// <param name="channel">The proxy channel to close.</param>
		private void CloseChannel( ProxyChannel channel )
		{
			// Close the channel.
			
			Logger.LogDebug( "Closing Channel." );

			channel.Close( );

			m_Channels.Remove( channel );
			
			Logger.LogDebug( "Channel closed." );
		}

		/// <summary>
		/// Accepts a client connection.
		/// </summary>
		/// <returns>A TcpSocketConnection.</returns>
		private TcpSocketConnection AcceptTcpClientConnection( )
		{
			Socket ClientSocket = m_ListeningSocket.Accept( );

			return CreateClientConnection( ClientSocket );
		}
		

		/// <summary>
		/// Disposes managed and unmanaged objects. 
		/// </summary>
		/// <param name="disposing">If <see langword="true"/> the method has been called directly
		/// or indirectly by a user's code. Managed and unmanaged resources
		/// can be disposed.
		/// If disposing equals <see langword="false"/>, the method has been called by the 
		/// runtime from inside the finalizer and you should not reference 
		/// other objects. Only unmanaged resources can be disposed.
		/// </param>
		private void Dispose( bool disposing )
		{
			if( !m_Disposed )
			{
				if( disposing )
				{
					// Dispose managed resources.
					
					Stop( );
				}
	             
				// Clean up unmanaged resources here.
			}

			m_Disposed = true;         
		}	
		
		/// <summary>
		/// Closes listening socket.
		/// </summary>
		private void CloseListeningSocket( )
		{
			// Close the listening socket.
			if( m_ListeningSocket != null )
			{
				m_ListeningSocket.Close( );
				m_ListeningSocket = null;
			}
		}
	}
}
